feat(seasons): real Season entity replaces legacy year-list (PR #612)#612
feat(seasons): real Season entity replaces legacy year-list (PR #612)#612barach6662001-bit merged 3 commits intomainfrom
Conversation
- Domain: Season (Code, Name, StartDate, EndDate, IsCurrent) : AuditableEntity
- EF: unique (TenantId, Code), partial unique (TenantId, IsCurrent=true),
CHECK (EndDate > StartDate), tenant soft-delete query filter
- Migration AddSeasons + idempotent data seed (3 default seasons per tenant,
Aug 1 -> Jul 31). DataSeeder backfills existing/test tenants at startup.
- /api/seasons rewritten: List/GetCurrent/Create/Update/SetCurrent/Delete,
CompanyAdmin or platform super-admin required for mutations. Delete blocks
on linked CostRecord/Sale/AgroOperation within the season range; super-admin
can force=true.
- /api/admin/tenants/{id}/seasons/* for platform super-admin, audited via
ISuperAdminAuditService.
- Breaking frontend API change: getSeasons() returns SeasonDto[] (was number[]).
Dashboard now navigates through real season records using StartDate/EndDate
instead of synthesising Jan 1 - Dec 31 year windows from transaction
timestamps.
- 6 integration tests covering tenant isolation, SetCurrent flip semantics,
delete safety rails, non-admin 403, and super-admin audit emission.
There was a problem hiding this comment.
💡 Codex Review
AgroPlatform/frontend/src/pages/Dashboard.tsx
Line 141 in a42942d
sortedSeasons to resolvedRangeLabel memo deps
resolvedRangeLabel reads sortedSeasons, but the dependency list omits it, so on the common path where seasons load asynchronously after the initial render (with unchanged period/fromDate) the memo stays on the fallback label and does not update to the active season name until some unrelated state change occurs. This leaves the dashboard showing an incorrect range label in season mode.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (await _db.Seasons.AnyAsync(s => s.TenantId == tenantId && s.Code == req.Code, ct)) | ||
| return Conflict(new { error = "Season with this code already exists." }); |
There was a problem hiding this comment.
Normalize season code before uniqueness checks
The duplicate check uses raw req.Code, but the entity is persisted with req.Code.Trim(), so inputs that differ only by surrounding whitespace (for example "2025/2026 ") bypass the AnyAsync conflict path and then hit the unique (TenantId, Code) index during SaveChanges, returning a server error instead of a 409. The same normalization mismatch appears in update logic too.
Useful? React with 👍 / 👎.
Summary
Replaces the legacy
/api/seasonsendpoint that returned a sortedint[]of years derived from transaction timestamps with a real tenant-scopedSeasonentity. This fixes the silent dashboard bug where users navigated through synthesised Jan 1 – Dec 31 calendar windows that never matched the actual crop cycle (Aug 1 → Jul 31 in UA).Backend
Season : AuditableEntity { Code(16), Name(100), StartDate: DateOnly, EndDate: DateOnly, IsCurrent: bool }.(TenantId, Code)filtered onIsDeleted=false; partial uniqueIX_Seasons_TenantId_IsCurrent_Uniquefiltered onIsCurrent=true AND IsDeleted=false(at most one current season per tenant); CHECKCK_Seasons_EndAfterStart; standard soft-delete query filter.AddSeasons: creates the table + indexes + check constraint, plus an idempotent raw-SQL seed that gives every active tenant three default seasons (2023/2024, 2024/2025, 2025/2026-current). Guarded byNOT EXISTSso replays are safe.DataSeeder.SeedDefaultSeasonsAsync: same idempotent backfill path for environments usingEnsureCreated()(tests, first-run dev)./api/seasons/*rewritten — tenant-scoped CRUD. Reads: any authenticated user of the tenant. Mutations:CompanyAdminor platform super-admin. Delete blocks on linkedCostRecord/Sale/AgroOperationrows falling inside the season's range; super-admin can override with?force=true.set-currentperforms a two-step flip (clear existing → set target) to avoid transient violation of the partial unique index./api/admin/tenants/{tenantId}/seasons/*— same CRUD under[SuperAdminRequired], bypasses tenant filter viaIgnoreQueryFilters(), emits oneSuperAdminAuditLogper mutation with before/after payloads.Frontend
getSeasons()now returnsSeasonDto[](wasnumber[]).Dashboard.tsx: navigation inperiod === 'season'mode now steps through realSeasonrecords by start date (using each season's actualstartDate/endDateas the window), not through synthesised year boundaries. The active season label usesseason.name(e.g. “Сезон 2025/2026”) instead of a bare year number.Tests
Six integration tests in
tests/AgroPlatform.IntegrationTests/Seasons/SeasonsTests.cs:List_ReturnsOnlySeasonsOfCurrentTenant— tenant isolation.SetCurrent_FlipsExactlyOneCurrentSeason— transactional flip invariant.Delete_WithLinkedCostRecord_Returns409ForTenantAdmin— safety rail.Delete_ForceByPlatformSuperAdmin_DeletesDespiteLinkedCostRecord— escape hatch.Create_AsNonAdmin_Returns403— authorization.SuperAdmin_Create_WritesExactlyOneAuditLogEntry— audit wiring.Unit tests: 309 passed, 0 failed.
Verification
Notes / follow-ups
plan.mdupdated accordingly.